Перейти к основному содержимому

4.15. Сборка мусора

Разработчику Архитектору Инженеру

Общие принципы

Сборщик мусора (Garbage Collector, GC) является ключевым компонентом сред выполнения управляемых языков. Несмотря на общую цель — автоматическое освобождение недостижимой памяти — реализации GC в различных языках отличаются по архитектуре, стратегии работы и степени возможности настройки.

GC определяет, какие объекты больше не достижимы из корней (глобальные переменные, локальные переменные в активных стеках, активные потоки). Доступные объекты считаются живыми; недостижимые — удаляются. Современные сборщики используют поколенческую модель, основанную на эмпирическом наблюдении: большинство объектов имеют короткий жизненный цикл («закон Больцмана»).

Мы уже немного затронули архитектуру выполнения кода, работу с памятью, но логичнее было бы разобрать сборку мусора отдельно по ключевым языкам, которые мы рассмотрели здесь - C#, Python, Java.

Сборщик мусора решает задачу управления памятью, но не освобождает разработчика от ответственности за проектирование ссылок. GC удаляет недостижимые объекты, но если объект остаётся достижимым — даже если он логически «мёртв» — он не будет собран. Это не ошибка GC, а ошибка управления жизненным циклом объектов. GC не знает о логической семантике приложения. Он видит только граф ссылок. Если ссылка на объект сохраняется — объект жив. Даже если он больше не нужен.

Сценарий: В игре игрок стреляет, создаётся объект Projectile. После столкновения с противником (OnCollision) объект должен быть удалён.

Ошибка:

// C# (Unity-подобный код)
void OnCollision(Enemy enemy)
{
// ... эффект взрыва
enemy.TakeDamage(damage);
// Забыли: this.gameObject.SetActive(false) или Destroy(this)
}

Или хуже:

public class ProjectileSpawner : MonoBehaviour
{
public List<Projectile> activeProjectiles = new List<Projectile>(); // ← статический или долгоживущий контейнер

void Spawn()
{
var p = new Projectile();
activeProjectiles.Add(p); // добавили
// ...
}
}

// В момент столкновения:
void OnCollision()
{
// this.Destroy(); — не вызвано
// и не удалено из activeProjectiles!
}

Последствия:

  • Объект Projectile остаётся в списке activeProjectiles → достижим → не удаляется GC.
  • Память накапливается с каждым выстрелом.
  • Через время — рост потребления памяти, частые сборки, фризы, OutOfMemoryError.

Решение - явное удаление ссылки:

spawner.activeProjectiles.Remove(this);

…а также использование Destroy(gameObject) (в Unity) — помечает объект как уничтоженный, движок сам управляет ссылками. И, конечно, пул объектов - переиспользование, а не создание/удаление.

GC здесь не виноват. Он правильно держит объект, потому что на него есть ссылка. Проблема — в архитектуре.

Сценарий: SPA (Single Page Application) на JS. При переходе между страницами компоненты создаются и должны удаляться.

Ошибка:

class UserProfile {
constructor() {
this.onResize = () => this.recalculateLayout();
window.addEventListener('resize', this.onResize);
}

destroy() {
// Забыли удалить обработчик!
// window.removeEventListener('resize', this.onResize);
}
}

Последствия:

  • Объект UserProfile привязан к window через addEventListener.
  • Даже если ссылка на объект потеряна в приложении, window по-прежнему хранит ссылку на this.onResize, а значит — и на this.
  • Объект недостижимым не становится → не удаляется GC.
  • При каждом открытии профиля — новый объект, новые обработчики → экспоненциальный рост памяти.

Решение - явное удаление:

destroy() {
window.removeEventListener('resize', this.onResize);
}

…или использование AbortController:

const controller = new AbortController();
window.addEventListener('resize', handler, { signal: controller.signal });
// ...
controller.abort(); // удаляет все обработчики

Когда программист «замечает» GC? GC становится заметным в следующих сценариях:

  1. Постоянный рост памяти. Мониторинг показывает, что RSS (Resident Set Size) растёт со временем, и возможная причина: объекты не освобождаются из-за сохранённых ссылок (в кэше, событиях, списках). Это утечка.
  2. Фризы / лаги интерфейса. Паузы в UI, особенно при интенсивных действиях. Возможная причина - частые или длительные GC-сборки (особенно full GC).
  3. Низкая производительность под нагрузкой, приложение тормозит при росте числа пользователей. Причина - высокое давление на GC из-за частых аллокаций.
  4. OutOfMemoryError / Out of heap space, приложение падает с ошибкой нехватки памяти. Причина - утечка или неадекватный размер кучи.

Выявляются такие проблемы через профилирование памяти:

  • C#: Visual Studio Diagnostic Tools, dotMemory, PerfView.
  • Java: JFR (Java Flight Recorder), VisualVM, Eclipse MAT.
  • JS: DevTools (Chrome) — Memory tab, Heap Snapshots, Allocation instrumentation.
  • Python: tracemalloc, objgraph, memory_profiler.

Методика такова - сначала нужно сделать снимок кучи до и после операции, затем найти объекты, которые не должны были остаться, и построить цепочку ссылок (retaining path), выяснить, кто их удерживает.

GC можно логировать:

  • Java: -Xlog:gc*:file.log
  • .NET: ETW + dotnet-trace
  • Python: gc.set_debug(gc.DEBUG_STATS)

Когда и зачем программист вызывает GC вручную? Явный вызов GC.Collect() (C#), System.gc() (Java), gc.collect() (Python) — крайняя мера. Может использоваться после массового освобождения ресурсов, например, закрытие уровня в игре:

UnloadLevel();
Resources.UnloadUnusedAssets();
GC.Collect(); // попытаться освободить сразу

Также может быть в тестах и бенчмарках, чтобы изолировать влияние GC на результаты. И в автономных сервисах с чёткими фазами, например, ETL-процесс: чтение → обработка → выгрузка. После чтения можно вызвать GC, чтобы начать обработку с «чистой» кучей.

В горячих путях, циклах, UI-потоке, без профилирования или как замену правильному управлению ссылками не стоит использовать принудительный вызов GC.

А когда и зачем настраивают GC? Программист настраивает GC не для ускорения, а для предсказуемости. Допустим, минимизация пауз ценой роста памяти, или гарантия, что паузы не превысят 5 мс, либо реже запускать полные сборки, чтобы не тормозить обработку запросов.

Давайте посмотрим, как реализуется GC в C#, Java и Python.

Реализация и настройка в .NET (C#)

В среде выполнения .NET управление памятью делегировано сборщику мусора (Garbage Collector, GC), который автоматически выделяет и освобождает память для управляемых объектов. При создании нового объекта Common Language Runtime (CLR) размещает его в управляемой куче (managed heap). Пока адресное пространство доступно, CLR продолжает выделение. Однако при исчерпании ресурсов или достижении пороговых значений запускается процесс сборки мусора, целью которого является освобождение памяти, занятой недостижимыми объектами.

Процесс сборки инициируется автоматически механизмом оптимизации GC на основе эвристик: объём аллоцированной памяти, частота создания объектов, состояние кучи. Разработчик не обязан управлять жизненным циклом памяти напрямую, однако понимание поведения GC необходимо для диагностики производительности и проектирования масштабируемых систем.

Куча в .NET организована по поколенческому принципу, основанному на гипотезе о коротком сроке жизни большинства объектов:

  • Поколение 0 (Gen 0) — содержит новые объекты. Сборка происходит часто и быстро.
  • Поколение 1 (Gen 1) — промежуточное хранилище для объектов, переживших первую сборку.
  • Поколение 2 (Gen 2) — долгоживущие объекты (например, кэши, глобальные сервисы). Сборка редкая, но затратная по времени.

Недостижимый объект — объект, на который отсутствуют прямые или косвенные ссылки из корней (roots). Корни включают глобальные переменные, локальные переменные в активных стеках вызовов, статические поля, параметры методов и активные потоки. Такой объект считается недоступным для программы и может быть безопасно удалён сборщиком мусора.

Исчерпание ресурсов — состояние, при котором система не может выделить дополнительную память из-за нехватки свободного адресного пространства или физической памяти. В контексте GC это может означать невозможность расширения управляемой кучи, что становится одной из причин инициации сборки.

Достижение пороговых значений — условие, при котором количество аллоцированной памяти в одном или нескольких поколениях кучи достигает внутреннего предела, установленного эвристиками GC. Например, если объём объектов в Gen 0 превышает порог (~256 КБ – несколько МБ, зависит от нагрузки), запускается сборка Gen 0. Пороги динамически корректируются на основе истории сборок и поведения приложения.

Инициация процесса сборки — начало работы сборщика мусора, вызванное либо автоматическими условиями (достигнут порог, нехватка памяти), либо явным запросом (GC.Collect()). На этом этапе GC определяет корни, строит граф достижимости и планирует освобождение памяти.

Эвристика — набор правил и алгоритмов, используемых GC для принятия решений о времени и масштабе сборки без полного анализа всей кучи. Например, GC может прогнозировать частоту создания объектов и адаптировать пороги поколений, чтобы минимизировать накладные расходы и паузы.

Также выделяется куча больших объектов (Large Object Heap, LOH) — отдельная область для объектов размером 85 КБ и более. Объекты в LOH не перемещаются при компактизации по умолчанию, что может приводить к фрагментации.

Начиная с .NET 4.8, возможна принудительная дефрагментация через:

GCSettings.LargeObjectHeapCompactionMode

Принудительная дефрагментация — операция, при которой блоки памяти в куче больших объектов (LOH) перемещаются для устранения фрагментации. Применяется при следующем вызове GC.Collect(). Не выполняется по умолчанию из-за высоких накладных расходов.

Режимы работы GC зависят от типа приложения:

  • Workstation GC - используется по умолчанию в клиентских приложениях. Оптимизирован для минимального влияния на отклик пользовательского интерфейса.
  • Server GC - активируется в серверных средах (например, ASP.NET). Создаёт отдельный поток GC для каждого ядра CPU, обеспечивая высокую пропускную способность за счёт параллелизма.

Режим задаётся в файле проекта:

<ServerGarbageCollection>true</ServerGarbageCollection>

или в runtimeconfig.json:

{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
}
}

Файл проекта — XML-файл с расширением .csproj, содержащий метаданные, зависимости, параметры компиляции и конфигурацию сборки проекта .NET. Используется системой MSBuild для управления процессом сборки и развертывания. В нём могут задаваться параметры среды выполнения, включая режим GC.

runtimeconfig.json — JSON-файл, генерируемый при публикации приложения .NET, содержащий параметры среды выполнения (runtime). Позволяет настраивать поведение CLR, включая использование серверного GC, максимальный размер кучи, уровень логирования и другие параметры, не требуя изменения исходного кода.

Фоновая и консервативная сборка.

Начиная с .NET 4.0, фоновая сборка (background GC) позволяет выполнять сборку Gen 2 без полной остановки всех потоков. В это время Gen 0 и Gen 1 продолжают обрабатываться конкурентно. Это снижает длительность пауз stop-the-world, особенно в приложениях с большой кучей.

Для задач, чувствительных к задержкам (например, игры, UI-интерактивные системы), доступны режимы задержки (GCLatencyMode):

  • Interactive — баланс между производительностью и откликом (по умолчанию).
  • LowLatency — минимизация пауз в течение короткого периода.
  • SustainedLowLatency — длительная работа с низкими паузами, но возможен рост потребления памяти.

Устанавливается программно:

GCSettings.LatencyMode = GCLatencyMode.LowLatency;

Режим задержки (latency mode) — настройка, определяющая приоритет GC между производительностью и длительностью пауз.

Задержки (latency) — временные интервалы, в течение которых выполнение управляемого кода приостанавливается из-за работы GC (stop-the-world).

Индукция и контроль сборки.

Индукция — процесс явного или неявного вызова сборки мусора. Может быть индуцирована автоматически (при нехватке памяти) или программно (через GC.Collect()). Термин используется в документации Microsoft: induced collection.

Контроль сборки — возможность разработчика влиять на поведение GC через API (GC.Collect, GC.TryStartNoGCRegion, GC.WaitForPendingFinalizers) или конфигурационные параметры. Не подразумевает полного управления, но позволяет задавать границы, приоритеты и реакцию на события.

Сборка может быть инициирована:

  1. Автоматически — при нехватке памяти или достижении порогов.
  2. Явно — вызовом GC.Collect(). Этот метод принимает параметры:
    • Поколение (GC.MaxGeneration для full GC),
    • Режим (GCCollectionMode.Forced, Optimized),
    • Флаг принудительной компактизации LOH.

Пример:

GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);

Рекомендация: явный вызов GC.Collect() следует использовать только в тестах или в крайних случаях (например, после массового освобождения ресурсов), так как он нарушает работу внутреннего планировщика GC и может снизить общую производительность.

Компактизация — процесс перемещения объектов в куче для устранения фрагментации и обеспечения непрерывного адресного пространства. Применяется в Gen 0, Gen 1 и Gen 2, но не в LOH по умолчанию. Улучшает локальность данных и эффективность последующих аллокаций.

Мониторинг и диагностика.

Для анализа поведения GC применяются:

  • Счётчики производительности (.NET Counters) — через dotnet-counters.
  • ETW-события — детальное логирование жизненного цикла сборок.
  • Visual Studio Diagnostic Tools, PerfView, dotMemory — анализ дампов памяти, выявление утечек.

ETW-события (Event Tracing for Windows) — механизм ядра Windows для высокопроизводительного логирования системных и прикладных событий. Сборщик мусора .NET генерирует ETW-события (например, GCStart, GCEnd, GCHeapStats), которые могут быть собраны инструментами вроде PerfView или WPR для детального анализа производительности GC.

dotnet-counters — кроссплатформенная утилита командной строки из пакета .NET CLI, позволяющая отслеживать значения счётчиков производительности в реальном времени, включая частоту сборок, объём кучи и аллокации в секунду. Данные поступают через .NET EventPipe, не требуя установки дополнительных зависимостей.

Дамп памяти (memory dump) — файл, содержащий снимок состояния процесса в определённый момент времени, включая управляемую и неуправляемую память, стеки потоков, регистры. Используется для анализа утечек памяти, зависаний и ошибок доступа. Может быть создан через procdump, dotnet-dump, Visual Studio или код с использованием MiniDumpWriteDump.

Утечка памяти — ситуация, при которой память, занятая объектами, не освобождается, хотя объекты больше не используются приложением. В .NET это происходит, когда на объект сохраняется живая ссылка (например, в статическом списке, обработчике события, кэше), делая его достижимым для GC, даже если он логически «мёртв». Приводит к постепенному росту потребления памяти.

Ключевые метрики:

  • Частота сборок по поколениям,
  • Длительность пауз (stop-the-world),
  • Размер кучи до и после сборки,
  • Объёмы аллокаций в единицу времени.

Частота сборок — количество операций сборки мусора за единицу времени, обычно измеряется отдельно для каждого поколения (Gen 0, Gen 1, Gen 2). Высокая частота Gen 0 может указывать на чрезмерные аллокации; частые Gen 2 — на утечки или неэффективное использование долгоживущих объектов.

Stop-the-world (STW) — состояние, при котором все потоки управляемого приложения приостанавливаются для выполнения критической фазы сборки мусора (например, root scanning, compaction). Длительность STW напрямую влияет на отзывчивость приложения. Современные GC стараются минимизировать STW за счёт фоновых и конкурентных операций.

Дескриптор (handle) — абстракция, предоставляемая операционной системой для доступа к неуправляемым ресурсам (файлы, сокеты, процессы, окна). Представляет собой целочисленный идентификатор, через который приложение взаимодействует с ядром ОС. Дескрипторы должны быть явно закрыты после использования, так как GC не управляет ими автоматически.

Уведомления о сборке.

API предоставляет возможность подписаться на приближение полной сборки:

if (GC.RegisterForFullGCNotification(10, 10))
{
// Ожидание сигнала о приближении GC
}

Это полезно в сценариях, где необходимо временно приостановить операции, чувствительные к паузам.

Освобождение ресурсов.

GC отвечает только за управление памятью. Для освобождения неуправляемых ресурсов (дескрипторы файлов, соединения, графические объекты) используется шаблон Dispose через интерфейс IDisposable. Метод Dispose() должен освобождать ресурсы немедленно, а Finalize() (деструктор) — резервный механизм на случай, если Dispose не был вызван.

Пример:

using var file = new FileStream("data.txt", FileMode.Open);
// Dispose вызывается автоматически

У Microsoft есть замечательная документация, которая имеет отдельный раздел, посвящённый управлению памятью, где и есть «Сбор мусора»:

https://learn.microsoft.com/ru-ru/dotnet/standard/garbage-collection/

Обязательно ознакомьтесь.

Реализация и настройка в Java

В среде выполнения Java (JVM) управление памятью осуществляется автоматически с помощью сборщика мусора (Garbage Collector, GC). При создании объекта память выделяется в куче (heap), управляемой JVM. По мере исчерпания свободного пространства или достижения пороговых значений инициируется процесс сборки, целью которого является освобождение памяти, занятой объектами, недостижимыми из корней (глобальные переменные, локальные переменные в активных стеках потоков).

Пороговое значение — числовое условие, при достижении которого инициируется определённое действие в системе. В контексте GC — это, например, количество аллоцированной памяти в регионе Eden (для G1) или объём "мусора" в поколении, превышение которого запускает сборку. Также используется в -XX:MaxGCPauseMillis как целевое значение для длительности пауз.

Исчерпание свободного пространства — состояние кучи, при котором недостаточно непрерывной или фрагментированной памяти для размещения нового объекта. Приводит к срабатыванию сборщика мусора. Если после сборки память не освобождается — возникает OutOfMemoryError. Стратегия сборки мусора — алгоритм управления памятью, реализованный конкретным сборщиком (G1, ZGC, Shenandoah). Определяет, как обнаруживаются недостижимые объекты, когда происходит сборка, как минимизируются паузы и используется ли параллелизм или конкурентность. Выбор стратегии сборки мусора оказывает прямое влияние на производительность приложения: пропускную способность (throughput), длительность пауз (latency) и потребление ресурсов. В отличие от унифицированной модели .NET, JVM предоставляет несколько реализаций GC, каждая из которых ориентирована на определённые сценарии использования. Разработчик может выбрать подходящий сборщик на этапе запуска приложения. Пропускная способность (throughput) — доля времени выполнения приложения, затрачиваемого на полезную работу, а не на сборку мусора. Например, если за 10 минут работы GC занимал 1 минуту, пропускная способность составляет 90%. Максимизация throughput — цель таких сборщиков, как Throughput Collector.

Длительность пауз (pause time) — временные интервалы, в течение которых все потоки приложения приостанавливаются (stop-the-world) для выполнения критических фаз GC (например, root scanning). Измеряется в миллисекундах. Ключевой показатель для систем реального времени. Потребление ресурсов — объём использования CPU, памяти и других системных ресурсов процессом JVM, включая накладные расходы GC. Конкурентные сборщики (ZGC, Shenandoah) потребляют больше CPU, но снижают длительность пауз.

Этап запуска приложения — момент, когда JVM инициализируется и начинает выполнение программы. На этом этапе можно задать параметры GC через аргументы командной строки (например, -XX:+UseZGC, -Xmx). Эти параметры фиксируются и не могут быть изменены в runtime. Ориентироваться будем на базу - OpenJDK. Там можно выделить следующие основные реализации сборщиков мусора:

  • G1 GC (Garbage-First Collector) — используется по умолчанию начиная с JDK 9. Предназначен для многопроцессорных систем с большим объёмом памяти. Куча разделяется на фиксированные регионы (regions), и сборщик приоритизирует те из них, где концентрация "мусора" максимальна ("garbage first"). Поддерживает поколенческую модель и позволяет задавать целевую длительность пауз через параметр -XX:MaxGCPauseMillis. Работает в режиме частичной конкурентности: некоторые фазы (например, marking) выполняются параллельно с приложением, другие — требуют stop-the-world.
  • ZGC (Z Garbage Collector) — низколатентный сборщик, разработанный для приложений, чувствительных к задержкам. Целевая длительность пауз — менее 10 мс, даже при объёме кучи до нескольких терабайт. Использует colored pointers и load barriers для асинхронной обработки ссылок. Большинство операций (marking, relocation) выполняется конкурентно с приложением. Полностью поколенческий начиная с JDK 15.
  • Shenandoah — аналог ZGC, разработанный независимо. Также обеспечивает низкие задержки за счёт полной конкурентности сборки. Использует forwarding pointers для перемещения объектов во время работы приложения. Поддерживает поколенческую модель с JDK 12+.
  • CMS (Concurrent Mark-Sweep) — устаревший сборщик, ориентированный на минимизацию длительности пауз. Удалён начиная с JDK 14. Не рекомендуется к использованию в новых проектах.
  • ря об упрощённых решениях вроде CMS, можно выделить еще несколько решений:
  • Serial GC: однопоточный сборщик для Young и Old поколений. Подходит для приложений с малым объёмом памяти (например, клиентские или встраиваемые системы). Паузы значительны.
  • Parallel GC (Throughput Collector): многопоточная версия Serial GC. Максимизирует пропускную способность за счёт параллельной обработки. Целевой сценарий — пакетная обработка, где допустимы длительные, но редкие паузы.

Достижимость объекта определяется через граф ссылок от корней. Объект считается живым, если существует путь от корня до него. В противном случае — мёртв и может быть собран.

Корни - это статические поля, локальные переменные в активных стеках потоков, параметры методов, JNI-указатели.

Концепция поколений основана на гипотезе о коротком сроке жизни большинства объектов:

  • Young Generation:
    • Eden: место создания новых объектов.
    • Survivor Spaces (S0/S1): временные области для объектов, переживших сборку.
  • Old Generation (Tenured): долгоживущие объекты, перешедшие из Young Gen после нескольких сборок.
  • Metaspace: заменил PermGen в Java 8, хранит метаданные классов.

G1 делит кучу на фиксированные регионы (обычно 1–32 МБ). Регионы могут быть назначены как Eden, Survivor или Old. Сборщик приоритизирует сборку регионов с высокой концентрацией мусора, что позволяет эффективно освобождать память без полной дефрагментации. Parallel GC максимизирует долю времени, затрачиваемого на выполнение полезной работы. ZGC и Shenandoah минимизируют длительность пауз, что критично для интерактивных систем. G1, ZGC, Shenandoah эффективны на многопроцессорных системах с большой памятью.

Концентрация мусора — доля недостижимых объектов в определённой области кучи (например, регионе G1). Сборщики, такие как G1, приоритизируют сборку регионов с высокой концентрацией мусора, чтобы максимизировать объём освобождаемой памяти за минимальное время. Целевая длительность пауз — параметр, задаваемый через -XX:MaxGCPauseMillis, который служит ориентиром для сборщика (например, G1) при планировании сборок. Не гарантирует соблюдение значения, но GC стремится к нему, регулируя объём обрабатываемой памяти за цикл. Частичная конкурентность — режим работы GC, при котором некоторые фазы (например, marking) выполняются параллельно с работой приложения, но критические этапы (например, final reference processing) требуют stop-the-world. Характерна для G1 и CMS.

Латентность (latency) — общее время от момента запроса до получения ответа. Влияет на отзывчивость приложения.

Низкая латентность (low latency) — минимально возможные задержки в отклике системы. В контексте GC — короткие и предсказуемые паузы. Достигается использованием сборщиков ZGC и Shenandoah.

Чувствительность к задержкам — характеристика приложения, при которой длительные паузы GC недопустимы. Типично для систем реального времени (HFT), игровых серверов, интерактивных UI. Требует использования low-latency GC (ZGC, Shenandoah).

Регионы (regions) — фиксированные блоки памяти, на которые делится куча в сборщиках G1, ZGC и Shenandoah. Позволяют гибко управлять распределением и сборкой памяти. Размер региона определяется автоматически (обычно от 1 МБ до 32 МБ).

Colored pointers — техника, используемая в ZGC, при которой часть битов указателя (указатели «окрашиваются») используется для хранения метаданных (например, состояния пометки). Позволяет выполнять marking и relocation без дополнительных структур данных, ускоряя обработку ссылок.

Forwarding pointers — временные указатели, используемые в Shenandoah для ссылки на новое местоположение объекта во время relocation. Позволяют перенаправлять доступы к объекту без остановки приложения.

Load barriers — точки в коде (вставляемые JIT-компилятором), где перед чтением ссылки выполняется дополнительная логика (например, перенаправление на перемещённый объект). Используются в ZGC и Shenandoah для обеспечения согласованности при конкурентном перемещении объектов.

Асинхронная обработка ссылок — выполнение операций с ссылками (например, обновление при relocation) без остановки всех потоков. Реализуется через load barriers и позволяет большинству фаз GC работать параллельно с приложением.

Marking — фаза GC, на которой определяются все достижимые объекты путём обхода графа ссылок от корней. Объекты помечаются как «живые». В конкурентных GC выполняется частично или полностью параллельно с приложением.

Relocation — фаза перемещения живых объектов в новые области кучи для дефрагментации. В G1 и ZGC выполняется конкурентно. Требует обновления всех ссылок на перемещённые объекты (через forwarding pointers или load barriers).

Root scanning — сканирование корневых ссылок (глобальные переменные, локальные переменные в стеках потоков, регистры CPU), с которых начинается построение графа достижимости. Эта фаза всегда требует stop-the-world, так как состояние корней должно быть зафиксировано.

Heap dump — файл, содержащий снимок управляемой кучи JVM в определённый момент времени. Включает все объекты, их поля, типы и связи. Используется для анализа утечек памяти с помощью инструментов: Eclipse MAT, VisualVM, JProfiler.

Поведение GC настраивается через аргументы командной строки JVM:

  • -XX:+UseG1GC - активация G1 GC
  • -XX:+UseZGC - активация ZGC (требует поддержки ОС и платформы)
  • -XX:+UseShenandoahGC - активация Shenandoah
  • -Xms<size> - начальный размер кучи
  • -Xmx<size> - максимальный размер кучи
  • -XX:MaxGCPauseMillis=<n> - целевое значение длительности паузы (для G1)
  • -XX:G1HeapRegionSize=<size> - размер региона в G1 (обычно 1–32 МБ, определяется автоматически)
  • -Xlog:gc*:file.log - логирование событий GC в файл (унифицированный логгер с JDK 9+)
  • -XX:+PrintGC - базовый вывод информации о GC (устаревший, предпочтителен -Xlog)

Пример запуска с ZGC:

java -XX:+UseZGC -Xmx10g -Xlog:gc:gc.log MyApp

Особенности поведения и ограничения

  • Stop-the-world паузы неизбежны полностью лишь в ZGC и Shenandoah, но даже они имеют короткие остановки на этапе root scanning. В G1 полные сборки (full GC) могут вызывать продолжительные паузы и должны быть исключены проектированием.
  • Масштабируемость: ZGC и Shenandoah эффективны при объёмах кучи от сотен гигабайт до терабайтов, тогда как G1 показывает предсказуемое поведение при объёмах до нескольких десятков гигабайт.
  • Потребление CPU: конкурентные GC (ZGC, Shenandoah) увеличивают нагрузку на CPU из-за работы load barriers и фоновых потоков.
  • Поддержка платформ: ZGC требует поддержки со стороны ОС (Linux/x64, Linux/AArch64, Windows/x64); Shenandoah доступен на большем числе платформ.

Для анализа производительности GC используются следующие инструменты:

  • jstat — утилита командной строки для мониторинга статистики GC в реальном времени.
  • jcmd — отправка диагностических команд JVM, включая принудительную сборку (GC.run) и дамп памяти.
  • Java Flight Recorder (JFR) — встроенный механизм записи событий выполнения, включая детализированные данные по GC. Активируется через -XX:+FlightRecorder.
  • VisualVM, Mission Control — графические инструменты для анализа heap dumps, профилирования и интерпретации GC-логов.

Логи GC (особенно с -Xlog:gc*,gc+heap=debug) позволяют анализировать:

  • Частоту и длительность сборок,
  • Объёмы освобождаемой памяти,
  • Фазы выполнения (mark, sweep, compact),
  • Причины инициации сборки (allocation failure, ergonomics).

Фазы выполнения — mark, sweep, compact

  • Mark — обход графа достижимости, пометка живых объектов.
  • Sweep — освобождение памяти, занятой непомеченными (мёртвыми) объектами. Не перемещает объекты.
  • Compact — перемещение живых объектов в непрерывный блок памяти для устранения фрагментации. Улучшает локальность и эффективность аллокаций.

Allocation failure — ситуация, при которой попытка выделить память под новый объект завершается неудачей из-за нехватки места в куче. Является одной из основных причин инициации full GC.

Ergonomics — набор автоматических механизмов JVM, адаптирующих параметры GC (размеры поколений, количество потоков) на основе характеристик платформы (число ядер, объём RAM) и поведения приложения. Цель — оптимальная производительность без ручной настройки.

Реализация и настройка в Python

В отличие от сред с виртуальными машинами, такими как JVM или CLR, управление памятью в CPython (стандартной реализации Python) основано на комбинации автоматических механизмов, интегрированных на уровне интерпретатора.

Основная стратегия — подсчёт ссылок (reference counting), дополненный сборщиком циклических ссылок для обнаружения недостижимых объектов в замкнутых графах. Эта двухуровневая модель обеспечивает предсказуемое освобождение большинства объектов, но имеет ограничения, влияющие на производительность и поведение в долгоживущих приложениях.

Ссылка — связь между именем (переменной, атрибутом, элементом контейнера) и объектом в памяти. В CPython реализована как указатель на структуру данных объекта. Каждый объект содержит счётчик ссылок, отслеживающий количество активных ссылок на него. Ссылки являются основным механизмом доступа к объектам.

Активный указатель — ссылка, находящаяся в области видимости и доступная для программы (например, локальная переменная в текущем стеке вызова, элемент списка в глобальной области). Пока существует хотя бы один активный указатель на объект, он считается достижимым и не может быть удалён сборщиком мусора.

Циклическая ссылка — ситуация, при которой два или более объекта взаимно ссылаются друг на друга, образуя замкнутый граф, при этом ни один из них не достижим извне (например, через глобальные или локальные переменные). Такие объекты не могут быть удалены подсчётом ссылок, так как их счётчики остаются больше нуля, несмотря на недостижимость.

Подсчёт ссылок — основной механизм управления памятью в CPython, при котором каждый объект хранит счётчик количества активных ссылок на него. При создании ссылки счётчик увеличивается (инкремент), при уничтожении — уменьшается (декремент). Когда счётчик достигает нуля, объект немедленно удаляется, а его память освобождается. Обеспечивает детерминированное освобождение для большинства объектов.

Сборщик циклических ссылок — дополнительный компонент управления памятью, реализованный в модуле gc. Предназначен для обнаружения и освобождения групп объектов, образующих циклы и недостижимых из корней (глобальные переменные, стеки потоков), но сохраняющих положительный счётчик ссылок. Работает периодически и не затрагивает объекты, не способные содержать циклы (например, строки, числа). Механизм подсчёта ссылок.

Каждый объект в CPython содержит счётчик ссылок, хранящий количество активных указателей на него. При создании объекта счётчик инициализируется значением 1. При присваивании ссылки на объект — счётчик увеличивается, при выходе из области видимости или переопределении переменной — уменьшается. Когда счётчик достигает нуля, объект немедленно уничтожается, а его память освобождается.

Пример:

a = []        # Счётчик списка: 1
b = a # Счётчик: 2
a = None # Счётчик: 1
b = None # Счётчик: 0 → объект удаляется

Подсчёт ссылок обеспечивает детерминированное освобождение памяти и минимальные паузы, что делает его эффективным для большинства сценариев. Однако он не способен обнаруживать циклические ссылки — ситуации, когда группа объектов ссылается друг на друга, но недостижима из глобальной области или активных стеков.

Сборщик циклических ссылок (gc module).

Для решения проблемы циклов используется модуль gc, реализующий дополнительный сборщик мусора, основанный на алгоритме обнаружения недостижимых графов. Он периодически сканирует объекты, способные содержать циклы (в основном контейнеры: списки, словари, классы), и определяет, можно ли достичь их из корней (глобальные переменные, локальные переменные в стеках потоков). Если объект недостижим — он помечается как мусор и удаляется.

Замкнутые графы — структуры объектов, в которых все узлы (объекты) связаны ссылками в замкнутую систему, и отсутствует путь от корней до любого из них. Являются объектами сборки для циклического GC. Пример: два экземпляра класса, ссылающиеся друг на друга через атрибуты, при условии, что на них больше нет внешних ссылок.

Счётчик — целочисленное поле в заголовке объекта CPython, хранящее количество активных ссылок. Управляется автоматически при операциях с ссылками.

Значение — данные, содержащиеся в объекте (например, строка "hello", число 42). Освобождается вместе с объектом после его удаления GC.

Детерминированное освобождение памяти — свойство подсчёта ссылок, при котором объект удаляется немедленно после того, как его счётчик достигает нуля. Это позволяет предсказуемо управлять временем жизни ресурсов (например, закрытие файлов), в отличие от недетерминированного GC, где момент удаления неизвестен.

Циклический GC не работает постоянно, а запускается по достижению пороговых значений аллокаций. По умолчанию используются три поколения (0, 1, 2), каждое со своим счётчиком и порогом. Новые объекты попадают в Gen 0. Если объект переживает сборку, он перемещается в следующее поколение. Более старшие поколения проверяются реже, что снижает накладные расходы.

Пороговые значения аллокаций — внутренние счётчики в модуле gc, определяющие, когда следует запустить сборку поколения. По умолчанию используются три порога (для Gen 0, Gen 1, Gen 2). Когда количество выделенных минус освобождённых объектов в Gen 0 превышает порог, инициируется сборка этого поколения. Значения можно настроить через gc.set_threshold().

Пороги настраиваются:

import gc
gc.set_threshold(700, 10, 10) # (Gen 0, Gen 1, Gen 2)

Управление сборкой.

Разработчик может управлять поведением сборщика через API модуля gc:

  • gc.collect() — принудительный запуск сборки всех поколений. Возвращает количество собранных объектов. Полезен после массового удаления данных.
  • gc.enable(), gc.disable() — включение и отключение автоматической сборки. Отключение может использоваться в критичных по производительности участках, где известно, что циклов не возникает.
  • gc.isenabled() — проверка состояния сборщика.
  • gc.get_objects() — получение списка всех отслеживаемых объектов (для отладки).
  • gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_LEAK) — включение режима отладки, выводящего информацию о статистике и потенциально утекающих объектах.

Ограничения и особенности реализации

  • Отсутствие поколенческой модели по умолчанию: хотя gc поддерживает поколения, они не интегрированы на уровне интерпретатора так же глубоко, как в .NET или JVM. Подсчёт ссылок остаётся основным механизмом.
  • Однопоточность: сборка выполняется в основном потоке. При длительной сборке выполнение приложения приостанавливается (stop-the-world).
  • Нет low-latency режимов: в отличие от ZGC или Shenandoah, CPython не предоставляет механизмов для минимизации пауз. Это критично для систем реального времени и высоконагруженных сервисов (например, в asyncio).
  • Фрагментация памяти: частые аллокации и освобождения могут приводить к фрагментации, особенно при работе с большими объектами.
  • Не все объекты участвуют в сборке: простые типы (int, str, tuple) управляются отдельными менеджерами памяти (например, малые блоки через arena allocator).

Малые блоки — фрагменты памяти небольшого размера (обычно до нескольких сотен байт), используемые для размещения малых объектов (например, целых чисел, коротких строк, кортежей). Управляются специализированным аллокатором (pymalloc) для повышения эффективности и снижения фрагментации.

Arena allocator — компонент менеджера памяти CPython, отвечающий за выделение больших областей памяти (арен), которые затем дробятся на страницы и блоки для размещения PyObject. Arena представляет собой крупную смежную область памяти (обычно 256 КБ), выделенная через malloc. Используется для централизованного управления памятью и упрощения деаллокации при завершении интерпретатора.

Pymalloc - специализированный аллокатор для малых объектов (<512 байт), минимизирующий фрагментацию. Системный malloc/free: для больших объектов.

Эта схема позволяет эффективно управлять памятью, но не предотвращает фрагментацию в долгоживущих процессах.